package org.dna.mybatis.criteria;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.reflection.MetaClass;
import org.apache.ibatis.session.Configuration;
import org.apache.log4j.Logger;

/**
 * Criteria that collects various Criterion and permit properties check against
 * the entity interested and binding to a MyBatis Configuration instance to 
 * convert property names to column names.
 * 
 * The typical usage should be:
 * 
 *<ul>
 * <li>Create an instance of Criteria with Criteria.create</li>
 * <li>Add some Criterion with util methods in {@link Restrictions}</li>
 * <li>Bind the Criteria to a MyBatis Session</li>
 * <li>Use the Criteria as parameter in predefined queries</li>
 *</ul>
 * 
 * <pre>
 * Criteria crit = Criteria.create(Activity.class); 
 * crit.add(Restrictions.lessThan("id", 3));
 * SqlSession session = //create a session
 * crit.bind(session.getConfiguration());
 * 
 * //use the session and the criteria in a query
 * List&lt;YourResult&gt; res = session.selectList("yourQuery", crit);
 * </pre>
 * 
 * @see Criterion
 * 
 * @author andrea
 */
public class Criteria<T> {

    private static final String RESULT_MAP_POSTFIX = "ResultMap";

    private Class<T> m_entityClass;
    private List<Criterion> m_criteria = new ArrayList<Criterion>();
    private MetaClass m_metaClass;
    private Configuration m_configuration;
    private Map<String, String> m_propertiesMap = new HashMap<String, String>();
    private static final Logger LOG = Logger.getLogger(Criteria.class.getName());

    private Criteria(Class<T> entity) {
        m_entityClass = entity;
        m_metaClass = MetaClass.forClass(m_entityClass);
    }

    public static synchronized <T> Criteria create(Class<T> entity) {
        return new Criteria(entity);
    }

    public Criteria add(Criterion crit) {
        m_criteria.add(crit);
        return this;
    }

    /**
     * Verify if all property names define din the various Criterion added to the
     * Criteria match the types/names in the entity passed into create.
     *
     * @return true if all the criteria added are verified.
     */
    public boolean verify() {
        for (Criterion crit : m_criteria) {
            if (!crit.verify(this)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Bind the set of criterion to the configuration to map the properties
     * with columns defined in the SqlMaps.
     * 
     * @throws CriteriaException if any properties can't be bound to some column.
     */
    public void bind(Configuration conf) throws CriteriaException {
        m_configuration = conf;
        buildAttributeMap();
        for (Criterion crit : m_criteria) {
            crit.bind(this);
        }
    }

    /**
     * Used by myBatis xml mapping queries
     */
    protected List<Criterion> getAllCriteria() {
        return m_criteria;
    }

    /***********************************
     *  API for Criterion implementations
     ***********************************/
    //NB it's a poor design mixing API and SPI in the same class!!
    /**
     * @return true iff the entity has got the property.
     */
    boolean hasProperty(String propName) {
        return m_metaClass.hasGetter(propName);
    }

    protected ResultMap findMapForType() {
        if (!isBound()) {
            throw new CriteriaException(String.format("Can't find a map for entity [%s]", m_entityClass));
        }
        for (ResultMap rm : m_configuration.getResultMaps()) {
            LOG.debug(String.format("Analizing resultMap: %s, ref class %s", rm.getId(), rm.getType()));
            if (rm.getType().equals(m_entityClass) && rm.getId().endsWith(RESULT_MAP_POSTFIX)) {
                return rm;
            }
        }
        throw new CriteriaException(String.format("Can't find a map for entity [%s]", m_entityClass));
    }

    /**
     * Return the column name binded to the passed propertyName, else throws
     * a CriteriaException.
     */
    protected String toColumnName(String propertyName) {
        String colName = m_propertiesMap.get(propertyName);
        if (colName == null) {
            throw new CriteriaException(String.format("Can't map the propertyName [%s] to any column in map", propertyName));
        }

        return colName;
    }
    
    /**
     * From ResultMap to associative map used in toColumnName;
     */
    private void buildAttributeMap() {
        ResultMap rm = findMapForType();
        LOG.debug(String.format("Using resultMap: %s for type: %s", rm.getId(), rm.getType()));
        for (ResultMapping singleMapping : rm.getResultMappings()) {
            m_propertiesMap.put(singleMapping.getProperty(), singleMapping.getColumn());
        }
        LOG.debug(String.format("Mappings for entity: %s map: %s",m_entityClass, m_propertiesMap));
    }

    /**
     * @return true iff the Entity.property is assignable to clazz.
     */
    boolean checkType(String propName, Class clazz) {
        Class getterType = m_metaClass.getGetterType(propName);
        if (getterType.isPrimitive()) {
            if (getterType.equals(Integer.TYPE)) {
                getterType = Integer.class;
            }
            if (getterType.equals(Boolean.TYPE)) {
                getterType = Boolean.class;
            }
            if (getterType.equals(Character.TYPE)) {
                getterType = Character.class;
            }
            if (getterType.equals(Byte.TYPE)) {
                getterType = Byte.class;
            }
            if (getterType.equals(Short.TYPE)) {
                getterType = Short.class;
            }
            if (getterType.equals(Long.TYPE)) {
                getterType = Long.class;
            }
            if (getterType.equals(Float.TYPE)) {
                getterType = Float.class;
            }
            if (getterType.equals(Double.TYPE)) {
                getterType = Double.class;
            }
            if (getterType.equals(Void.TYPE)) {
                getterType = Void.class;
            }
        }
        return getterType.isAssignableFrom(clazz);
    }

    private boolean isBound() {
        return m_configuration != null;
    }
}
